#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
staticDHCPd
===========
Highly customisable, static-lease-focused DHCP server.

Legal
-----
This file is part of staticDHCPd.
staticDHCPd is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

(C) Neil Tallim, 2014 <flan@uguu.ca>
"""
import logging
import logging.handlers
import os
import signal
import sys
import time
import traceback

import staticdhcpdlib
import libpydhcpserver

parser_options = None
#Options-processing needs to be done before config is loaded
if __name__ == '__main__' and len(sys.argv) > 1:
    import optparse
    parser = optparse.OptionParser()
    parser.add_option("--config", help="specify the location of conf.py", dest="config")
    parser.add_option("--debug", help="output logging information at the DEBUG level", dest="debug", action="store_true", default=False)
    parser.add_option("--verbose", help="disable daemon mode, if set, and enable console output", dest="verbose", action="store_true", default=False)
    parser.add_option("--version", help="display version information", dest="version", action="store_true", default=False)
    (parser_options, args) = parser.parse_args()
    if parser_options.version:
        print(
            "staticDHCPd v" + staticdhcpdlib.VERSION + " - " + staticdhcpdlib.URL +
            " | libpydhcpserver v" + libpydhcpserver.VERSION + " - " + libpydhcpserver.URL
        )
        sys.exit(0)
    if parser_options.config:
        os.environ['STATICDHCPD_CONF_PATH'] = parser_options.config
    del parser
    del args
    del optparse
#Options-processing complete

import staticdhcpdlib.config

_logger = logging.getLogger('main')

def _gracefulShutdown():
    """
    Attempts to shut down the daemon cleanly on the first call, but ends the
    process if a second is made.
    """
    if staticdhcpdlib.system.ALIVE:
        _logger.warn("System shutdown beginning...")
        staticdhcpdlib.system.ALIVE = False
    else:
        _logger.warn("System shutting down immediately")
        sys.exit(1)
        
def _termHandler(*args):
    """
    Cleanly shuts down this daemon upon receipt of a SIGTERM.
    """
    _logger.warn("Received SIGTERM")
    _gracefulShutdown()
    
def _hupHandler(*args):
    """
    Reinitialises the system upon receipt of a SIGHUP.
    """
    _logger.warn("Received SIGHUP")
    staticdhcpdlib.system.reinitialise()
    
def _daemonise():
    """
    The process of daemonising, the standard Unix way.
    """
    if os.fork(): #The first fork, to decouple stuff
        sys.exit(0)
    os.setsid() #Ensure session semantics are configured
    os.chdir('/') #Avoid holding references to unstable resources
    
    #And, lastly, clean up the base descriptors
    devnull = os.open('/dev/null', os.O_RDWR)
    os.dup2(devnull, sys.stdin.fileno())
    os.dup2(devnull, sys.stdout.fileno())
    os.dup2(devnull, sys.stderr.fileno())
    
    if os.fork(): #The second fork, to ensure TTY cannot be reacquired
        sys.exit(0)
        
def _setupLogging():
    """
    Attaches handlers to the root logger, allowing for universal access to
    resources.
    """
    logging.root.setLevel(logging.DEBUG)
    
    if staticdhcpdlib.config.DEBUG:
        formatter = logging.Formatter(
            "%(asctime)s : %(levelname)s : %(name)s:%(lineno)d[%(threadName)s] : %(message)s"
        )
    else:
        formatter = logging.Formatter(
            "%(asctime)s : %(levelname)s : %(message)s"
        )
        
    if not staticdhcpdlib.config.DAEMON: #Daemon-style execution disables console-based logging
        if logging.root.handlers:
            _logger.info("Configuring console-based logging...")
        console_logger = logging.StreamHandler()
        console_logger.setLevel(getattr(logging, staticdhcpdlib.config.LOG_CONSOLE_SEVERITY))
        console_logger.setFormatter(formatter)
        logging.root.addHandler(console_logger)
        _logger.info("Console-based logging online")
        
    if staticdhcpdlib.config.LOG_FILE: #Determine whether disk-based logging is desired
        if logging.root.handlers:
            _logger.info("Configuring file-based logging for " + staticdhcpdlib.config.LOG_FILE + "...")
        if staticdhcpdlib.config.LOG_FILE_HISTORY:
            #Rollover once per day, keeping the configured number of days' logs as history
            file_logger = logging.handlers.TimedRotatingFileHandler(
             staticdhcpdlib.config.LOG_FILE, 'D', 1, staticdhcpdlib.config.LOG_FILE_HISTORY
            )
            if logging.root.handlers:
                _logger.info("Configured rotation-based logging for file, with history=" + str(staticdhcpdlib.config.LOG_FILE_HISTORY) + " days")
        else:
            #Keep writing to the specified file forever
            file_logger = logging.FileHandler(staticdhcpdlib.config.LOG_FILE)
            if logging.root.handlers:
                _logger.info("Configured indefinite-growth logging for file")
        file_logger.setLevel(getattr(logging, staticdhcpdlib.config.LOG_FILE_SEVERITY))
        file_logger.setFormatter(formatter)
        logging.root.addHandler(file_logger)
        _logger.info("File-based logging online")
        
    if staticdhcpdlib.config.EMAIL_ENABLED: #Add an SMTP handler
        smtp_handler = logging.handlers.SMTPHandler(
            (staticdhcpdlib.config.EMAIL_SERVER, staticdhcpdlib.config.EMAIL_PORT),
            staticdhcpdlib.config.EMAIL_SOURCE,
            staticdhcpdlib.config.EMAIL_DESTINATION,
            staticdhcpdlib.config.EMAIL_SUBJECT,
            credentials=(staticdhcpdlib.config.EMAIL_USER and (staticdhcpdlib.config.EMAIL_USER, staticdhcpdlib.config.EMAIL_PASSWORD) or None)
        )
        if logging.root.handlers:
            _logger.info("Configured SMTP-based logging for " + staticdhcpdlib.config.EMAIL_DESTINATION + " via " + staticdhcpdlib.config.EMAIL_SERVER + ":" + str(staticdhcpdlib.config.EMAIL_PORT))
        smtp_handler.setLevel(logging.CRITICAL)
        smtp_handler.setFormatter(formatter)
        logging.root.addHandler(smtp_handler)
        _logger.info("SMTP-based logging online")
        
def _initialise():
    """
    Loads and configures system components.
    """
    import staticdhcpdlib.system
    
    if staticdhcpdlib.config.WEB_ENABLED:
        _logger.info("Webservice module enabled; configuring...")
        import staticdhcpdlib.web
        import staticdhcpdlib.web.server
        webservice = staticdhcpdlib.web.server.WebService()
        webservice.start()
        
        import staticdhcpdlib.web.methods
        import staticdhcpdlib.web.headers
        staticdhcpdlib.web.registerHeaderCallback(staticdhcpdlib.web.headers.contentType)
        staticdhcpdlib.web.registerMethodCallback('/javascript', staticdhcpdlib.web.methods.javascript, cacheable=(not staticdhcpdlib.config.DEBUG))
        staticdhcpdlib.web.registerHeaderCallback(staticdhcpdlib.web.headers.javascript)
        
        if staticdhcpdlib.config.WEB_LOG_HISTORY > 0:
            _logger.info("Webservice logging module enabled; configuring...")
            web_logger = staticdhcpdlib.web.methods.Logger()
            staticdhcpdlib.web.registerDashboardCallback('core', 'events', web_logger.render, staticdhcpdlib.config.WEB_DASHBOARD_ORDER_LOG)
        
        if staticdhcpdlib.config.WEB_REINITIALISE_ENABLED:
            staticdhcpdlib.web.registerMethodCallback(
                '/ca/uguu/puukusoft/staticDHCPd/reinitialise', staticdhcpdlib.web.methods.reinitialise,
                hidden=staticdhcpdlib.config.WEB_REINITIALISE_HIDDEN, module='core', name='reinitialise',
                secure=staticdhcpdlib.config.WEB_REINITIALISE_SECURE, confirm=staticdhcpdlib.config.WEB_REINITIALISE_CONFIRM,
                display_mode=staticdhcpdlib.web.WEB_METHOD_DASHBOARD
            )
            
        if staticdhcpdlib.config.WEB_HEADER_TITLE:
            staticdhcpdlib.web.registerHeaderCallback(staticdhcpdlib.web.headers.title)
            
        if staticdhcpdlib.config.WEB_HEADER_CSS:
            staticdhcpdlib.web.registerMethodCallback('/css', staticdhcpdlib.web.methods.css, cacheable=(not staticdhcpdlib.config.DEBUG))
            staticdhcpdlib.web.registerHeaderCallback(staticdhcpdlib.web.headers.css)
            
        if staticdhcpdlib.config.WEB_HEADER_FAVICON:
            staticdhcpdlib.web.registerMethodCallback('/favicon.ico', staticdhcpdlib.web.methods.favicon, cacheable=(not staticdhcpdlib.config.DEBUG))
            staticdhcpdlib.web.registerHeaderCallback(staticdhcpdlib.web.headers.favicon)
            
def _initialiseDHCP():
    """
    Loads and configured DHCP system components.
    """
    import staticdhcpdlib.system
    
    #Ready the database.
    import staticdhcpdlib.databases
    database = staticdhcpdlib.databases.get_database()
    staticdhcpdlib.system.registerReinitialisationCallback(database.reinitialise)
    
    #Start the DHCP server.
    import staticdhcpdlib.dhcp
    dhcp = staticdhcpdlib.dhcp.DHCPService(database)
    dhcp.start()
    staticdhcpdlib.system.registerTickCallback(dhcp.tick)
    
if __name__ == '__main__':
    if parser_options and parser_options.debug:
        staticdhcpdlib.config.DEBUG = True
        staticdhcpdlib.config.LOG_FILE_SEVERITY = 'DEBUG'
        staticdhcpdlib.config.LOG_CONSOLE_SEVERITY = 'DEBUG'
        print("staticDHCPd: Debugging overrides enabled: debugging operation requested")
        
    if staticdhcpdlib.config.DAEMON:
        if parser_options and parser_options.verbose:
            staticdhcpdlib.config.DAEMON = False
            print("staticDHCPd: Daemonised execution disabled: verbose operation requested")
        else:
            _daemonise()
    del _daemonise
    
del parser_options #No longer needed; allow reclamation

if __name__ == '__main__':
    _setupLogging()
    del _setupLogging
    for i in (
     "----------------------------------------",
     "----------------------------------------",
     "----------------------------------------",
     "System startup in progress; PID=" + str(os.getpid()),
     "staticDHCPd version " + staticdhcpdlib.VERSION + " : " + staticdhcpdlib.URL,
     "libpydhcpserver version " + libpydhcpserver.VERSION + " : " + libpydhcpserver.URL,
     "Continuing with subsystem initialisation",
     "----------------------------------------",
    ):
        _logger.warn(i)
    del i
    
    pidfile_recorded = False
    if staticdhcpdlib.config.PID_FILE:
        _logger.debug("Writing pidfile...")
        try:
            if os.path.isfile(staticdhcpdlib.config.PID_FILE):
                pidfile = open(staticdhcpdlib.config.PID_FILE, 'r')
                _logger.warn("Pidfile already exists, with PID " + pidfile.read().strip())
                pidfile.close()
                
            pidfile = open(staticdhcpdlib.config.PID_FILE, 'w')
            pidfile.write(str(os.getpid()) + '\n')
            pidfile.close()
            os.chown(staticdhcpdlib.config.PID_FILE, staticdhcpdlib.config.UID, staticdhcpdlib.config.GID)
        except:
            _logger.error("Unable to write pidfile: %(file)s" % {'file': staticdhcpdlib.config.PID_FILE,})
        else:
            pidfile_recorded = True
            
    try:
        #Set signal-handlers.
        signal.signal(signal.SIGHUP, _hupHandler)
        _logger.debug("Installed SIGHUP handler")
        signal.signal(signal.SIGTERM, _termHandler)
        _logger.debug("Installed SIGTERM handler")
        
        #Initialise all system resources
        _initialise()
        del _initialise
        
        _logger.info("Initialising custom code...")
        staticdhcpdlib.config.init()
        
        #Initialise the DHCP server
        _initialiseDHCP()
        del _initialiseDHCP
        
        _logger.info("Changing runtime permissions to UID=%(uid)i, GID=%(gid)i..." % {
         'uid': staticdhcpdlib.config.UID,
         'gid': staticdhcpdlib.config.GID,
        })
        os.setregid(staticdhcpdlib.config.GID, staticdhcpdlib.config.GID)
        os.setreuid(staticdhcpdlib.config.UID, staticdhcpdlib.config.UID)
        
        #By this point, all extensions have had an opportunity to alias things
        #and configure themselves, so try to reclaim memory
        del staticdhcpdlib.config.conf.extensions
        
        _logger.warn("----------------------------------------")
        _logger.warn("All subsystems initialised; now serving")
        _logger.warn("----------------------------------------")
        sleep_offset = 0
        while staticdhcpdlib.system.ALIVE:
            time.sleep(max(0.0, 1.0 - sleep_offset))
            
            start_time = time.time()
            staticdhcpdlib.system.tick()
            sleep_offset = time.time() - start_time
    except KeyboardInterrupt:
        _logger.warn("System shutdown requested via keyboard interrupt")
    except Exception:
        _logger.critical("System shutdown triggered by unhandled exception:\n" + traceback.format_exc())
    finally:
        _gracefulShutdown()
        if pidfile_recorded:
            _logger.debug("Unlinking pidfile...")
            try:
                os.unlink(staticdhcpdlib.config.PID_FILE)
            except:
                _logger.error("Unable to unlink pidfile: %(file)s" % {'file': staticdhcpdlib.config.PID_FILE,})
                
